← 都来码 / Dolaima← 例程索引swift/app/plane.swift原文

swift/app/plane.swift · 24475 字节 · 下载原文

// 飞机空战 —— 用 **Swift** 写的手机游戏

// Plane combat — a phone game written in **Swift**

//

// ✅ 能玩。判据是**逐像素量**出来的:本机(青色)随方向键左右移动(左右各 -16px、上下各 -10px)、

// ✅ Playable. The criteria were **measured pixel by pixel**: the player plane (cyan) moves left/right with the arrow keys (16px each way horizontally, 10px vertically),

//    子弹(黄色)匀速上行、敌机(红色)下行、打中后敌机消失且分数条变长、

//    bullets (yellow) travel upward at a constant speed, enemy planes (red) come down, a hit removes the enemy and lengthens the score bar,

//    **漏掉 3 架就结束**(右上角三架小飞机逐架灭掉)并弹对话框重开。

//    and **missing 3 planes ends the game** (the three little planes in the top-right go out one by one) with a dialog to restart.

//

// ◆ 玩法

// ◆ Gameplay

//

// 星空滚动,本机在下方,**自动开火**(手机上按住方向键就够忙的,不必再按射击)。

// A starfield scrolls, the player plane sits at the bottom, and **firing is automatic** (holding the arrow keys on a phone is busy enough without also pressing fire).

// 方向键左右移动(上下也能动,只是范围小)。SELECT 暂停,回车重开,ESC 或返回箭头退出。

// Arrow keys move left/right (up/down work too, just with a smaller range). SELECT pauses, Enter restarts, ESC or the back arrow exits.

// 打下一架 +10 分并略微提速;**放跑一架扣一条命**(共 3 条),用完就结束。

// Shooting one down is +10 points and a slight speed-up; **letting one escape costs a life** (3 in total), and the game ends when they run out.

//

// ⚠ 为什么是"扣命"而不是"撞机才死":第一版写的是撞机才结束,实测 **500 拍一次都没输过** ——

// ⚠ Why "lose a life" instead of "die on collision": the first version ended only on a crash, and measured over **500 ticks it never lost once** —

//    子弹是沿本机那一列往上的,本机不动就是一个"子弹墙",那一列上的敌机在顶端就被打掉了,

//    bullets travel up the player's own column, so a stationary player is a "wall of bullets" and enemy planes in that column get shot down at the very top,

//    永远走不到本机跟前。于是游戏既输不了、也很难得分。改成"放跑就扣命"之后,

//    never reaching the player. The game could neither be lost nor scored well. After switching to "escape costs a life",

//    玩家必须**横向跑位去拦**每一架,才成为一个真正的游戏(实测 500 拍内 3 局、最高 40 分)。

//    the player must **move sideways to intercept** every plane, which is what makes it a real game (measured: 3 rounds within 500 ticks, best score 40).

//

// ⚠ 另有一处**只有写游戏才会踩到**的:敌机槽位定成 6 个之后,"敌机 × 本机 / 敌机出底"

// ⚠ There is another one you **only step on when writing a game**: after the enemy slot count was set to 6, the "enemy × player / enemy off the bottom"

//    那一圈忘了跟着从 `4` 改成 `foeSlots()` ⇒ 第 5、6 号槽的敌机**永不回收、也永不扣命**,

//    loop was not changed from `4` to `foeSlots()` too ⇒ the planes in slots 5 and 6 were **never recycled and never cost a life**,

//    槽位占满之后**再也没有敌机出现**(静默地"游戏自己停了":画面还在动、分数不涨)。

//    and once the slots were full **no more enemy planes appeared** (a silent "the game stopped by itself": the picture still moved but the score stopped rising).

//    **凡是"槽位数"这类常量,定下来之后要 grep 一遍所有遍历它的循环。**

//    **Whenever a constant like "slot count" is fixed, grep for every loop that iterates over it.**

//

// ◆ 踩过的两个坑(都记在文件头,免得下次再踩)

// ◆ The two pitfalls hit (both recorded in the file header, so they are not hit again)

//

// ① **只能有一个全局数组**(`docs/前端游戏能力评估.md` §4.5)。实测同一程序里开

// ① **There can be only one global array** (frontend game-capability assessment doc, §4.5). Measured: opening

//    `var A / var B / var C` 三个全局数组会**互相踩内存** —— 蛇那次的表现是"只画出一节、

//    three global arrays `var A / var B / var C` in one program makes them **clobber each other's memory** — in the Snake case it showed as "only one segment drawn,

//    HUD 文字错位"。所以这里所有状态都塞进一个 `A`,靠下标分区(见下面那张表)。

//    HUD text misaligned". So all state here is packed into a single `A`, partitioned by index (see the table below).

// ② **带括号的表达式曾经恒等于 0**(`(k) * 2` 编成 `move R0 #0`,patch 0011)——

// ② **Parenthesized expressions used to be identically 0** (`(k) * 2` compiled to `move R0 #0`, patch 0011) —

//    症状是"算式下标不能复用"。已修,但写的时候仍然只用**最朴素的算式下标**。

//    the symptom was "computed indices cannot be reused". Fixed, but this still writes only the **plainest computed indices**.

//    (✅ 2026-10-04 复验仍成立:`ui_2d_line(16 + (k) * 2, …)`(k=5)实测落在 x=26。)

//    (✅ Re-checked 2026-10-04, still true: `ui_2d_line(16 + (k) * 2, …)` (k=5) measures x=26.)

//

// ◆ 为什么是 Swift

// ◆ Why Swift

//

// 22 个前端逐个过筛(编译出图 / 循环 / 数组 / 函数 / 参数传递)之后,Swift 是**最干净**的那一门:

// After screening all 22 frontends one by one (compiles and draws / loops / arrays / functions / argument passing), Swift is the **cleanest** one:

// 直接 `call` 到包装标签、实参逆序压栈、循环分支数组读写带参函数返回值、定时器心跳,

// it `call`s the wrapper labels directly, pushes arguments in reverse order, handles loops, branches, array reads/writes and functions with arguments and return values, and the timer heartbeat,

// 而且**字符串拼接可用**(`"a" + "b"`、`numToStr`),所以这里的分数能直接写出数字。

// and **string concatenation works** (`"a" + "b"`, `numToStr`), so the score here can be written out as digits directly.

//

// ⚠ 它也不是全须全尾:**第 5 个形参起,取到的全是第 1 个形参的值**(v0.96.195 修,patch 0011)——

// ⚠ It is not flawless either: **from the 5th parameter on, every one reads back the value of the 1st** (fixed in v0.96.195, patch 0011) —

//   8 参的 `overlap` 因此恒判"相交",敌机一冒头就被打掉。写多参函数之前值得知道这一条。

//   so the 8-argument `overlap` always judged "intersecting" and enemy planes were shot down the moment they appeared. Worth knowing before writing multi-argument functions.

//

// 数值的唯一真源仍是 `Lib/c/vml_ui.h`。

// The single source of truth for these numbers is still `Lib/c/vml_ui.h`.


// 状态表(只用一个数组,见文件头坑①)

//   0=px 1=py 2=score 3=best 4=alive 5=paused 6=speed 7=tickMs

//   8=starOff 9=sw 10=sh 11=fireCd 12=hits 13=wave

//   20..23=子弹 x,24..27=子弹 y,28..31=子弹在飞

//   20..23=bullet x, 24..27=bullet y, 28..31=bullet in flight

//   40..43=敌机 x,44..47=敌机 y,48..51=敌机在飞

//   40..43=enemy x, 44..47=enemy y, 48..51=enemy in flight

//   60..67=星星 x,68..75=星星 y

//   60..67=star x, 68..75=star y

//   110=界面语言(0=中文 1=英文)—— 数组刻意留到 112 个,就是给这个槽用的

//   110=UI language (0=Chinese 1=English) — the array is deliberately sized to 112 just for this slot

//   (按文件头 ① 的实测,语言码**不能**用模块级标量存:读出来是垃圾)

//   (per the measurement in header ①, the language code **cannot** be stored in a module-level scalar: it reads back as garbage)

#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]

func colorSky() -> Int { return -15986144 }      // 0xFF0C1220

func colorStar() -> Int { return -14011056 }     // 0xFF2A3550

func colorShip() -> Int { return -12922369 }     // 0xFF3AD1FF

func colorBullet() -> Int { return -8090 }       // 0xFFFFE066

func colorEnemy() -> Int { return -50384 }       // 0xFFFF3B30

func colorHud() -> Int { return -6643536 }       // 0xFF9AA0B0

func colorBar() -> Int { return -11409298 }      // 0xFF51E86E

func colorBar2() -> Int { return -63488 }        // 0xFFFF0800

func colorWhite() -> Int { return -1 }

func msgKeydown() -> Int { return 1 }
func msgTimer() -> Int { return 9 }
func msgClose() -> Int { return 10 }

func keyEnter() -> Int { return 13 }
func keySelect() -> Int { return 16 }
func keyEsc() -> Int { return 27 }
func keyLeft() -> Int { return 37 }
func keyUp() -> Int { return 38 }
func keyRight() -> Int { return 39 }
func keyDown() -> Int { return 40 }

func anchorLeft() -> Int { return 0 }
func anchorCenter() -> Int { return 1 }
func anchorRight() -> Int { return 2 }

// 版面常量(都是算出来的,不在多处各写一遍)

// Layout constants (all derived, not written out in several places)

func shipW() -> Int { return 34 }
func shipH() -> Int { return 26 }
func bulW() -> Int { return 3 }
func bulH() -> Int { return 10 }
func foeSlots() -> Int { return 6 }     // 敌机槽位:必须 > 一次下落期间的刷新次数,否则会刷满后停摆

// enemy slots: must exceed the number of spawns during one descent, otherwise the slots fill up and it stalls

func foeW() -> Int { return 28 }
func foeH() -> Int { return 22 }
func topBar() -> Int { return 46 }        // HUD 之下才是活动区

// the play area starts below the HUD


func shipY() -> Int { return A[10] - 56 }

// 位图数字(Swift 的字符串拼接可用,但这里只拼数字,标点保持列距一致)

// Bitmap digits (Swift's string concatenation works, but only digits are concatenated here; punctuation keeps the column spacing consistent)

func numToStr(v: Int) -> String {
    if v == 0 { return "0" }
    var s = ""
    var n = v
    if n < 0 { n = 0 - n }
    while n > 0 {
        var d = n % 10
        if d == 0 { s = "0" + s }
        else if d == 1 { s = "1" + s }
        else if d == 2 { s = "2" + s }
        else if d == 3 { s = "3" + s }
        else if d == 4 { s = "4" + s }
        else if d == 5 { s = "5" + s }
        else if d == 6 { s = "6" + s }
        else if d == 7 { s = "7" + s }
        else if d == 8 { s = "8" + s }
        else { s = "9" + s }
        n = n / 10
    }
    if v < 0 { s = "-" + s }
    return s
}

// 打中一架:分数 +10,每 50 分提一档速度

// Shot down one: score +10, and one speed step every 50 points

func addScore() {
    A[2] = A[2] + 10
    if A[2] > A[3] { A[3] = A[2] }
    A[12] = A[12] + 1
    if A[12] % 5 == 0 {
        if A[6] < 14 { A[6] = A[6] + 1 }
        if A[7] > 34 { A[7] = A[7] - 4 }
    }
}

func resetStars() {
    var i = 0
    while i < 8 {
        A[60 + i] = ui_math_rand(A[9])
        A[68 + i] = ui_math_rand(A[10] - topBar()) + topBar()
        i = i + 1
    }
}

func reset() {
    A[0] = A[9] / 2
    A[1] = shipY()
    A[2] = 0
    A[4] = 1
    A[5] = 0
    A[6] = 8
    A[7] = 46
    A[8] = 0
    A[11] = 0
    A[12] = 0
    A[13] = 0
    A[14] = 3
    var i = 0
    while i < 4 {
        A[28 + i] = 0
        i = i + 1
    }
    i = 0
    while i < foeSlots() {
        A[48 + i] = 0
        i = i + 1
    }
    resetStars()
}

// 找空槽发射一颗子弹(自动开火,手机上不必再按射击)

// Find a free slot and fire a bullet (automatic fire, no need to press shoot on a phone)

func fire() {
    var i = 0
    while i < 4 {
        if A[28 + i] == 0 {
            A[20 + i] = A[0]
            A[24 + i] = A[1] - 6
            A[28 + i] = 1
            ui_audio_beep(2093, 33)   // 射击:最轻最短(单音 ui_audio_beep,v0.96.509 从音序器换回来)

            // shoot: the lightest and shortest (single-tone ui_audio_beep, switched back from the sequencer in v0.96.509)

            return
        }
        i = i + 1
    }
}

func spawnFoe() {
    var i = 0
    while i < foeSlots() {
        if A[48 + i] == 0 {
            A[40 + i] = ui_math_rand(A[9] - 60) + 30
            A[44 + i] = topBar() - foeH()
            A[48 + i] = 1
            return
        }
        i = i + 1
    }
}

func hitFoe(slot: Int) {
    A[48 + slot] = 0
    addScore()
    ui_audio_beep(1319, 165)   // 击落:一声高而短的「叮」

    // shot down: a short, high "ding"

}

func drawShip() {
    var x = A[0]
    var y = A[1]
    // 机身

    // Fuselage

    ui_2d_rect(x - 5, y - shipH() / 2, 10, shipH(), colorShip(), 1, 0, 3)
    // 机翼

    // Wings

    ui_2d_rect(x - shipW() / 2, y, shipW(), 7, colorShip(), 1, 0, 2)
    // 尾焰(每拍换一点,看起来在喷)

    // Exhaust flame (changes a little each tick, so it looks like it is burning)

    var f = A[8] % 2
    if f == 0 { ui_2d_rect(x - 2, y + shipH() / 2, 4, 6, colorBullet(), 1, 0, 0) }
    else { ui_2d_rect(x - 2, y + shipH() / 2, 4, 9, colorBullet(), 1, 0, 0) }
}

func drawFoe(slot: Int) {
    var x = A[40 + slot]
    var y = A[44 + slot]
    ui_2d_rect(x + 6, y, foeW() - 12, foeH(), colorEnemy(), 1, 0, 3)
    ui_2d_rect(x, y + 6, foeW(), 7, colorEnemy(), 1, 0, 2)
    ui_2d_rect(x + 10, y + 5, 8, 5, colorSky(), 1, 0, 1)
}

func draw() {
    ui_2d_clear(colorSky())

    // 星空:8 颗,每拍按自己的速度下移,出底就回到顶

    // Starfield: 8 stars, each moving down at its own speed per tick, returning to the top when it leaves the bottom

    var i = 0
    while i < 8 {
        var sy = A[68 + i] + 2 + i % 3
        if sy > A[10] - 4 { sy = topBar() }
        A[68 + i] = sy
        ui_2d_rect(A[60 + i], sy, 3, 3, colorStar(), 1, 0, 0)
        i = i + 1
    }

    // 子弹

    // Bullets

    i = 0
    while i < 4 {
        if A[28 + i] != 0 { ui_2d_rect(A[20 + i] - 1, A[24 + i], bulW(), bulH(), colorBullet(), 1, 0, 1) }
        i = i + 1
    }

    // 敌机

    // Enemy planes

    i = 0
    while i < foeSlots() {
        if A[48 + i] != 0 { drawFoe(i) }
        i = i + 1
    }

    drawShip()

    // HUD:分数条 / 最高分条 + 右上角写数字(Swift 拼字符串是好的)

    // HUD: score bar / best-score bar + digits in the top-right (string concatenation in Swift is fine)

    // 界面语言(0=中文 1=英文):从状态表里取个局部量 —— 模块级标量读出来是垃圾(见文件头 ①)。

    // UI language (0=Chinese 1=English): pull it from the state table into a local — a module-level scalar reads back as garbage (see header ①).

    var lang = A[110]
    ui_2d_text(8, 8, lang == 0 ? "得分" : "Score", colorHud(), 13, anchorLeft())
    var bar = A[2]
    if bar > 160 { bar = 160 }
    ui_2d_rect(58, 11, bar, 10, colorBar(), 1, 0, 0)
    ui_2d_text(A[9] / 2, 8, lang == 0 ? "最高" : "Best", colorHud(), 13, anchorCenter())
    var bar2 = A[3]
    if bar2 > 160 { bar2 = 160 }
    ui_2d_rect(A[9] / 2 + 46, 11, bar2, 10, colorBar2(), 1, 0, 0)
    ui_2d_text(A[9] - 8, 8, numToStr(A[2]), colorWhite(), 14, anchorRight())
    // 剩余命数:右上角画几架小飞机(不是数字 —— 一眼看得出还剩几条)

    // Lives left: draw a few little planes in the top-right (not a number — you can see at a glance how many are left)

    var lf = 0
    while lf < A[14] {
        ui_2d_rect(A[9] - 24 - lf * 16, 26, 10, 8, colorShip(), 1, 0, 2)
        lf = lf + 1
    }

    if A[5] != 0 { ui_2d_text(A[9] / 2, A[10] / 2, lang == 0 ? "暂停(SELECT 继续)" : "Paused (SELECT)", colorEnemy(), 16, anchorCenter()) }
    ui_2d_present()
}

func overlap(ax: Int, ay: Int, aw: Int, ah: Int, bx: Int, by: Int, bw: Int, bh: Int) -> Int {
    if ax + aw < bx { return 0 }
    if bx + bw < ax { return 0 }
    if ay + ah < by { return 0 }
    if by + bh < ay { return 0 }
    return 1
}

func gameOver() {
    A[4] = 0
    ui_audio_beep(131, 320)   // 死(结局):**最低音**、最长

    // dead (ending): **the lowest note**, the longest one

    draw()
    var lang = A[110]
    if ui_dlg_msg(lang == 0 ? "飞机空战" : "Air Combat", lang == 0 ? "被撞到了,这一局结束。\n再来一局?(选「否」退出)" : "You crashed. Round over.\nPlay again? (choose 'No' to quit)", VML_DLG_INFO) != 0 { ui_win_close(); return }
    reset()
}

// 返回 0 表示本拍无变化、不必重画

// Returning 0 means nothing changed this tick and no redraw is needed

func step() -> Int {
    if A[4] == 0 { return 0 }
    if A[5] != 0 { return 0 }

    A[8] = A[8] + 1

    // 自动开火

    // Automatic fire

    A[11] = A[11] - 1
    if A[11] <= 0 {
        fire()
        A[11] = 5 - A[6] / 3
        if A[11] < 2 { A[11] = 2 }
    }

    // 子弹上行

    // Bullets travel up

    var i = 0
    while i < 4 {
        if A[28 + i] != 0 {
            A[24 + i] = A[24 + i] - 14
            if A[24 + i] < topBar() - 10 { A[28 + i] = 0 }
        }
        i = i + 1
    }

    // 敌机下行

    // Enemy planes travel down

    i = 0
    while i < foeSlots() {
        if A[48 + i] != 0 { A[44 + i] = A[44 + i] + A[6] }
        i = i + 1
    }

    // 刷敌机

    // Spawn enemy planes

    A[13] = A[13] + 1
    if A[13] >= 22 - A[6] {
        A[13] = 0
        spawnFoe()
    }

    // 子弹 × 敌机

    // Bullet × enemy

    var b = 0
    while b < 4 {
        if A[28 + b] != 0 {
            var e = 0
            while e < foeSlots() {
                if A[48 + e] != 0 {
                    if overlap(A[20 + b] - 1, A[24 + b], bulW(), bulH(), A[40 + e], A[44 + e], foeW(), foeH()) != 0 {
                        A[28 + b] = 0
                        hitFoe(e)
                        e = foeSlots()
                    }
                }
                e = e + 1
            }
        }
        b = b + 1
    }

    // 敌机 × 本机 / 敌机出底

    // Enemy × player / enemy off the bottom

    // ⚠ 这一圈必须走 foeSlots():写成 4 的话,第 5、6 号槽的敌机**永远不被清、也永远不扣命**

    // ⚠ This loop must go through foeSlots(): written as 4, the planes in slots 5 and 6 are **never cleared and never cost a life**

    //   —— 它们掉出屏幕后槽位一直占着,刷到第 3 架就再也没有敌机了(静默地"游戏自己停了")。

    //   — after they fall off screen their slots stay occupied, and by the 3rd spawn there are no more enemy planes at all (a silent "the game stopped by itself").

    i = 0
    while i < foeSlots() {
        if A[48 + i] != 0 {
            if overlap(A[0] - shipW() / 2, A[1] - shipH() / 2, shipW(), shipH(), A[40 + i], A[44 + i], foeW(), foeH()) != 0 {
                gameOver()
                return 1
            }
            if A[44 + i] > A[10] - 20 {
                A[48 + i] = 0
                A[14] = A[14] - 1
                // 被撞:低闷的一声(原来 43 = 98Hz 在 C3 以下 —— 按判据抬到 165,

                // Hit: a low, muffled note (it used to be 43 = 98Hz, below C3 — raised to 165 per the criterion,

                //   与"死"那条 131 也差得开)

                //   still far enough from the 131 used for "dead")

                ui_audio_beep(165, 66)
                if A[14] <= 0 {
                    gameOver()
                    return 1
                }
            }
        }
        i = i + 1
    }

    return 1
}

func main() {
    // 界面语言:开局问一次宿主要中文还是英文(0=中文 1=英文),存进 A[110](见 draw)。

    // UI language: ask the host once at the start whether it wants Chinese or English (0=Chinese 1=English) and store it in A[110] (see draw).

    // ⚠ 别在每帧里调 —— 那是一次 syscall。

    // ⚠ Don't call it in every frame — that's a syscall.

    A[110] = ui_scr_get_language()
    var lang = A[110]
    // 开窗:尺寸传 0 ⇒ 交给宿主给真实可用区(开窗**之前**问到的只是估算值)

    // Opening: pass size 0 ⇒ the host supplies the real available area (a **pre-open** read is only an estimate)

    ui_win_open(lang == 0 ? "飞机空战" : "Air Combat", 0, 0)

    // 尺寸:开窗**之后**再问一次 —— 这时 `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[9] = ui_scr_get_w()
    A[10] = ui_scr_get_h()
    if A[9] <= 0 { A[9] = 360 }
    if A[10] <= 0 { A[10] = 620 }

    ui_scr_set_keep_on(1)
    reset()
    draw()

    var tid = ui_timer_set(A[7], 0)
    var curMs = A[7]
    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(0)

        // 这一帧该走几步:**按墙钟欠账**(各平台定时器分辨率不同 ⇒ 不能拿"收到几拍"当时间)。

        // 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 > A[7] * 8 { acc = 0 }   // 卡太久别一次补一大串(防瞬移)

                                     // Do not catch up in one huge burst after a long stall (anti-teleport)

        while acc >= A[7] {
            acc = acc - A[7]
            if step() != 0 { draw() }
        }

        if t == msgClose() { break }
        if t == msgTimer() {
            // 定时器**只负责唤醒**(推进在上面按墙钟欠账走)

            // The timer **only wakes us up** — the advance happens above, by wall-clock debt

            // 提速后要把定时器换掉(重复定时器的间隔是建立时定死的)

            // After speeding up you must replace the timer (a repeating timer's interval is fixed when it is created)

            if A[7] != curMs {
                ui_timer_kill(tid)
                curMs = A[7]
                tid = ui_timer_set(curMs, 0)
            }
        }
        else if t == msgKeydown() {
            var k = ui_msg_get_a()
            if k == keyEsc() { break }
            else if k == keyLeft() { if A[0] > 24 { A[0] = A[0] - 16 } }
            else if k == keyRight() { if A[0] < A[9] - 24 { A[0] = A[0] + 16 } }
            else if k == keyUp() { if A[1] > A[10] - 150 { A[1] = A[1] - 10 } }
            else if k == keyDown() { if A[1] < shipY() + 40 { A[1] = A[1] + 10 } }
            else if k == keyEnter() { reset() }
            else if k == keySelect() { if A[5] != 0 { A[5] = 0 } else { A[5] = 1 } }
        }
        // 每次收到按键就先画一帧(手机上按方向键要立刻看到飞机动)

        // Draw a frame as soon as a key arrives (on a phone pressing an arrow key must move the plane immediately)

        if t == msgKeydown() { draw() }
    }

    ui_timer_kill(tid)
    ui_scr_set_keep_on(0)
    ui_win_close()
}