多点触控与设备
多指查询、按住判断、方向锁、全屏。
完整清单见「UI 开发」。下面每个接口都带一句用法和一个例子;签名取自
Lib/c/vml_ui.h(权威来源)。
ui_audio_is_playing(void)
后台 BGM 还在放吗 → 1/0(没放过、已放完、被停掉都是 0)。典型用法:等一首放完再进下一段。
ui_audio_play("bgm.mp3", 0);
while (ui_audio_is_playing()) { ui_msg_wait(msg, 200); }
ui_scr_set_immersive(int on)
隐藏 / 恢复状态栏与导航栏(全屏游戏)。
ui_scr_set_immersive(1); /* 进游戏:全屏 */
ui_scr_set_immersive(0); /* 退出前恢复 */
ui_key_is_down(int key)
某个键此刻按住没有(“持续按住左移”不必自己维护状态表)。 ⚠ 它治不了丢 KeyUp —— 手指划出按键范围、系统吃掉 CANCEL 都会让 KeyUp 永远不来,而查询会一直报“按着”。连发逻辑仍然要自带刹车(按了没动两次就停 / 总拍数上限)。
if (ui_key_is_down(VML_KEY_LEFT)) { x = x - 2; }
ui_scr_lock_orientation(int mode)
锁定屏幕方向:VML_LOCK_PORTRAIT(0) / VML_LOCK_LANDSCAPE(1) / VML_LOCK_AUTO(2)。 ⚠ 与 ui_win_open_ex 的 rotatable 是两件事:那个管“转屏时窗口跟不跟”,这个管“系统让不让转”。
ui_scr_lock_orientation(VML_LOCK_LANDSCAPE); /* 运行中锁成横屏 */
ui_touch_read(int slot, int* out)
查第 slot 根手指(0..9):写进 out[0]=x out[1]=y out[2]=按下,返回 1 有效(槽位越界 0)。 双人同屏 / 虚拟摇杆 / 双指缩放 / 按和弦靠它。⚠ 这是轮询,要自己每帧查。
int t[3];
if (ui_touch_read(0, t) && t[2]) { /* 第 0 根手指按在 (t[0],t[1]) */ }
⚠⚠ 触摸的「事件消息」只有槽位 0 会投 —— ui_msg_wait 收到的触摸消息永远只描述第 0 根手指。所以凡是多指的程序,按下/抬起的判定必须走轮询(挂个 16~33ms 的定时器,自己用上一拍的按下状态做边沿检测)。只靠事件消息的话,程序在单指下一切正常、 一旦同时按第二根手指,第二根就完全看不见 —— 这也正是"按住琴键的另一根手指点不动按钮"那类怪现象的来源。现成写法见 examples/c/app/piano.c。
ui_touch_is_down(void)
上一次 ui_touch_fetch 缓存里的“按下”(1/0)。
if (ui_touch_is_down()) { /* … */ }
ui_touch_fetch(int slot)
拿不到指针的语言用这个:查一次并缓存,下面三个读缓存(与 ui_msg_fetch_wait + ui_msg_get_a/b 同一套分工)。
ui_touch_fetch(0);
if (ui_touch_is_down()) { x = ui_touch_get_x(); y = ui_touch_get_y(); }
ui_touch_get_x(void)
上一次 ui_touch_fetch 缓存里的 x。
int x = ui_touch_get_x();
ui_touch_get_y(void)
上一次 ui_touch_fetch 缓存里的 y。
int y = ui_touch_get_y();
传感器(歪手机控制)
ui_sensor_read(int kind, int* out)
读最新读数,写进 out[0..2]。返回 1 = 有效、0 = 这台设备没有该传感器。
kind 三选一 —— 做倾斜控制用第一个:
| kind | 是什么 | 单位 |
|---|---|---|
VML_SENS_ACCEL(0) | 加速度(含重力) | 毫克(1000 = 1g) |
VML_SENS_GYRO(1) | 角速度(转得多快) | 千分之一度/秒 |
VML_SENS_ROTATION(2) | 融合姿态(俯仰/翻滚/方位) | 千分之一度 |
int a[3];
if (ui_sensor_read(VML_SENS_ACCEL, a)) {
/* 平放时 a ≈ (0, 0, 1000);往右歪 30° 时 a[0] ≈ 500 */
move_x = 0 - a[0] / 40; /* ⚠ 取负:球往低处滚 */
}
⚠⚠ 为什么倾斜用加速度计而不是陀螺仪:陀螺仪给的是角速度,要"当前角度"得自己积分,而积分会漂(几十秒就偏出可用范围)。加速度计静止时读到的就是重力方向,也就是倾斜本身,不漂。
⚠⚠ 两个轴都要取负号。加速度计读的是"哪条轴朝上"—— 把右边抬起来 x 会变大,而东西该往低的那边滑。写反的症状是"歪这边、东西往那边跑",看起来完全像传感器装反了,其实就错在这一行。(这条已在真机上确认:右边抬高、球往左滚。)
⚠ 读不到 ≠ 读到 0:返回 0 才是"没有这个传感器"。混在一起程序会以为"手机放平了"。
ui_sensor_is_available(int kind)
这台设备有没有该传感器 → 1/0。开窗之前就能问。
ui_sensor_set_rate(int kind, int ms)
采样间隔(毫秒,0 = 平台默认)。传感器一开就在耗电,间隔越大越省。
ui_sensor_calibrate(int kind)
把当前姿态当成零点("校准水平")。玩家躺在沙发上玩时"水平"是错的,退出重进也还是错的 ⇒ 给一个"点一下校准"的入口。
ui_sensor_fetch(int kind) / ui_sensor_get_x/y/z(void)
拿不到指针的语言用这一套(与 ui_touch_fetch 同构):先 ui_sensor_fetch(0) 查一次并缓存,再读 ui_sensor_get_x() 等。
ok = ui_sensor_fetch(0);
if (ok) { x = ui_sensor_get_x(); y = ui_sensor_get_y(); z = ui_sensor_get_z(); }
现成例子:examples/c/app/tilt.c(水平仪 + 小球,三个传感器的读数都画在屏幕上)。
电量与省电
玩到一半没电是手机游戏最常见的"事故",而程序自己就能防 —— 电量低了少画点东西、关掉音效、提醒玩家存档。不需要任何权限。
ui_battery_read(int* out)
读电量与充电状态:out[0] = 电量 0–100、out[1] = 充电中(1)/没充电(0)。返回 1 = 有效、0 = 这台设备没有电池(桌面、一直插电的机器)。
int b[2];
if (ui_battery_read(b) && b[0] < 15 && b[1] == 0) {
low_power_mode(); /* 没在充电、又是低电量 ⇒ 该省了 */
}
⚠ 「没有电池」与「电量 0%」是两回事:返回 0 才是前者。拿 100 或 0 冒充,程序会以为"电很足"或"电耗尽了",而真相是"这个问题在这里没有意义"。
⚠ 电量变得很慢(几分钟才动一格)⇒ 在"每拍跑一次"的逻辑里顺手看一眼就够, 不必每帧查。
ui_power_is_saving(void)
系统的省电模式开着吗 → 1/0。
⚠ 与"电量低"不是一回事:那是用户明确要求省电(也可能在电量还有 40% 时手动开的)。程序据此降帧是照用户的意思办,而不是你在猜。两者互相独立 —— 满电也可能省电模式开着。
ui_battery_fetch(void) / ui_battery_get_level(void) / ui_battery_is_charging(void)
拿不到指针的语言用这一套(与 ui_touch_fetch 同构):先 ui_battery_fetch() 查一次并缓存,再读 ui_battery_get_level() 等。
现成例子:examples/c/app/tilt.c 底部就画着电量那一行。
剪贴板与分享
手机游戏的标配:分享成绩、存取档码(把整局状态编成一小段文本,玩家自己存到备忘录里,换设备接着玩)。都不需要权限。
char code[64];
ui_clipboard_set("SAVE-1a2b3c"); /* 把存档码交给玩家 */
if (ui_clipboard_get(code, 64) > 0) { /* 读回来接着玩 */ }
ui_share_text("我在拧螺丝里拧了 8 圈!", "分享战绩");
ui_clipboard_set(char* text) / ui_clipboard_get(char* buf, int cap)
写 / 读系统剪贴板。set 返回 1=已交给系统、0=这一端没有剪贴板; get 返回写入的 UTF-8 字节数、-1=剪贴板是空的。
⚠ 返回值 1 只表示"已经交给系统",不表示"玩家已经存上了" —— 写剪贴板在平台上是异步的,宿主不等结果。 ⚠ 缓冲区不够时 get 返回的是实际写入的字节数(不是原文长度)—— **拿它和 strlen 比一比就知道有没有被截断**。 ⚠ 传空串给 set 不会清空剪贴板(返回 0)—— 那是"没算出来",不是你想要的"清空"。
ui_share_text(char* text, char* title)
弹系统分享面板(标题传空串 = 不带)。1=已弹出、0=这一端没有分享面板。 ⚠ 不阻塞 —— 只负责把面板弹出来,程序继续跑。
ui_url_open(char* url)
用系统浏览器打开链接。**只认 http:// 和 https://**,别的 scheme 一律返回 0 (放行任意 scheme 等于让程序借系统去打开任意东西)。
⚠ 它会离开这个应用 —— 调之前先存档。但别指望宿主替你暂停:那是策略,不是机制。
⚠ 返回 0 是"这一端没有这个能力",不是失败 —— 桌面就没有分享面板。游戏该照常继续(与"设备没有陀螺仪"同一条处置)。
错误码(上一次调用为什么失败)
相机 / 相册、文件选择器、定位这几条失败时一律返回 0 —— 而 0 分不清三件事: 用户点了取消 / 没有权限 / 这一端压根没有这个能力。这两个接口给它们分家:
char img[256];
if (ui_camera_capture(img, sizeof(img)) > 0) {
ui_2d_image(0, 0, img, 320, 240);
} else if (ui_error_get_code() != VML_E_CANCELLED) {
char why[64];
ui_error_get_message(why, sizeof(why)); /* 人话,例如「没有权限」 */
ui_dlg_msg("取图失败", why, VML_DLG_WARN);
}
| 值 | 名字 | 含义 |
|---|---|---|
| 0 | VML_E_OK | 没有错误(上一次调用成功了) |
| 1 | VML_E_CANCELLED | 用户取消了 —— 不是故障 |
| 2 | VML_E_PERMISSION | 没有权限(被拒 / "不再询问") |
| 3 | VML_E_UNSUPPORTED | 这一端没有这个能力(没相机 / 没 GPS / 没这个传感器) |
| 4 | VML_E_TIMEOUT | 超时 |
| 5 | VML_E_FAILED | 其它失败(宿主内部错误 / 异常 / 文件不存在) |
ui_error_get_code(void)
最近一次失败的错误码;没有错误就是 0。
⚠ 它描述的是「刚才那一次宿主调用」—— 中间再调任何别的 ui_*,槽就被清回 0,所以要在失败之后立刻读。这两个读口自己不清槽,先问码再问消息读的是同一次失败。
if (ui_gps_fetch(5000) == 0) {
if (ui_error_get_code() == VML_E_PERMISSION) { /* 引导用户去系统设置里开权限 */ }
}
ui_error_get_message(char* buf, int cap)
把人话描述写进 buf(最多 cap 字节,含结尾 0),返回写入的字节数;没有错误就写空串、返回 0。cap 传 0 = 只问长度(一个字节都不写)。描述跟随界面语言(中文端中文、英文端英文),可以直接拿去给玩家看。
⚠ VML_E_CANCELLED 是玩家的正常操作,别当故障终止程序。