← Dolaima← Docs indexMulti-touch and device原文中文

Multi-touch and device

Multi-finger queries, held-key tests, orientation lock, full screen.

For the full list see "UI development". Each call below comes with a one-line usage note and an example; signatures come from Lib/c/vml_ui.h (the authoritative source).

ui_audio_is_playing(void)

Is the background BGM still playing? -> 1/0 (never played, already finished and stopped all give 0). Typical use: wait for one track to finish before moving to the next segment.

ui_audio_play("bgm.mp3", 0);
while (ui_audio_is_playing()) { ui_msg_wait(msg, 200); }

ui_scr_set_immersive(int on)

Hides / restores the status bar and navigation bar (for full-screen games).

ui_scr_set_immersive(1);   /* entering the game: full screen */
ui_scr_set_immersive(0);   /* restore before quitting */

ui_key_is_down(int key)

Is a key currently held down (so "hold left to keep moving left" does not need your own state table). Note: it does not cure a lost KeyUp - a finger sliding off the key, or the system eating a CANCEL, can mean the KeyUp never arrives, and the query then reports "held" forever. Auto-repeat logic still needs its own brake (stop after two ticks with no movement / a cap on the total number of ticks).

if (ui_key_is_down(VML_KEY_LEFT)) { x = x - 2; }

ui_scr_lock_orientation(int mode)

Locks the screen orientation: VML_LOCK_PORTRAIT(0) / VML_LOCK_LANDSCAPE(1) / VML_LOCK_AUTO(2). Note: this is a different thing from ui_win_open_ex's rotatable: that one governs "does the window follow a rotation", this one governs "does the system allow rotation at all".

ui_scr_lock_orientation(VML_LOCK_LANDSCAPE);   /* lock to landscape while running */

ui_touch_read(int slot, int* out)

Queries finger number slot (0..9): it writes out[0]=x out[1]=y out[2]=pressed and returns 1 when valid (and 0 when the slot is out of range). Two players on one screen / virtual joysticks / pinch zoom / chords rely on it. Note: this is polling, so you have to check it yourself every frame.

int t[3];
if (ui_touch_read(0, t) && t[2]) { /* finger 0 is pressing at (t[0],t[1]) */ }

Note: touch event messages are only posted for slot 0 - a touch message received by ui_msg_wait always describes finger 0 only. So any program that handles multiple fingers must do its press/release detection by polling (set a 16-33ms timer and do your own edge detection against the pressed state from the previous tick). If you rely only on event messages, everything works with one finger and as soon as a second finger goes down the second one is completely invisible - which is exactly where oddities like "the other finger holding a piano key cannot tap a button" come from. See examples/c/app/piano.c for a ready-made pattern.

ui_touch_is_down(void)

The "pressed" value (1/0) from the cache of the last ui_touch_fetch.

if (ui_touch_is_down()) { /* ... */ }

ui_touch_fetch(int slot)

For languages that cannot get a pointer: query once and cache the result, then the three calls below read the cache (the same split of labour as 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)

The x from the cache of the last ui_touch_fetch.

int x = ui_touch_get_x();

ui_touch_get_y(void)

The y from the cache of the last ui_touch_fetch.

int y = ui_touch_get_y();

Sensors (tilt control)

ui_sensor_read(int kind, int* out)

Reads the latest reading into out[0..2]. Returns 1 = valid, 0 = this device has no such sensor.

kind is one of three - use the first for tilt control:

kindWhat it isUnit
VML_SENS_ACCEL(0)Acceleration (including gravity)milli-g (1000 = 1g)
VML_SENS_GYRO(1)Angular velocity (how fast it is turning)thousandths of a degree per second
VML_SENS_ROTATION(2)Fused attitude (pitch / roll / azimuth)thousandths of a degree
int a[3];
if (ui_sensor_read(VML_SENS_ACCEL, a)) {
    /* lying flat, a is about (0, 0, 1000); tilted 30 degrees to the right, a[0] is about 500 */
    move_x = 0 - a[0] / 40;     /* note the minus: the ball rolls downhill */
}

Note: why tilt uses the accelerometer and not the gyroscope. The gyroscope gives angular velocity, so getting "the current angle" means integrating it yourself - and integration drifts (within tens of seconds it is outside the usable range). At rest the accelerometer reads the direction of gravity, which is the tilt itself, and it does not drift.

Note: both axes need a minus sign. The accelerometer reads "which axis points up" - lift the right side and x grows, while things should slide toward the low side. Get it backwards and the symptom is "tilt this way and things run that way", which looks exactly like a sensor mounted upside down when the only mistake is that one line. (This has been confirmed on a real device: raise the right side and the ball rolls left.)

Note: no reading is not the same as a reading of 0: only a return of 0 means "there is no such sensor". Mix the two up and the program thinks "the phone is lying flat".

ui_sensor_is_available(int kind)

Does this device have that sensor? -> 1/0. You can ask before opening a window.

ui_sensor_set_rate(int kind, int ms)

Sampling interval (milliseconds, 0 = the platform default). A sensor draws power as soon as it is on, so a longer interval saves more.

ui_sensor_calibrate(int kind)

Treats the current attitude as the zero point ("level calibration"). When a player is lounging on the sofa, "level" is wrong, and quitting and coming back does not fix it - so give them a "tap to calibrate" entry point.

ui_sensor_fetch(int kind) / ui_sensor_get_x/y/z(void)

For languages that cannot get a pointer (structured like ui_touch_fetch): call ui_sensor_fetch(0) once to query and cache, then read ui_sensor_get_x() and so on.

ok = ui_sensor_fetch(0);
if (ok) { x = ui_sensor_get_x(); y = ui_sensor_get_y(); z = ui_sensor_get_z(); }

A ready-made example: examples/c/app/tilt.c (a spirit level plus a small ball, with all three sensors drawn on screen).

Battery and power saving

Running out of power halfway through is the most common "accident" in phone games, and a program can guard against it on its own - draw less when the battery is low, turn off sound effects and remind the player to save. No permissions are needed.

ui_battery_read(int* out)

Reads the battery level and charging state: out[0] = level 0-100, out[1] = charging(1)/not charging(0). Returns 1 = valid, 0 = this device has no battery (a desktop, or a machine that is always plugged in).

int b[2];
if (ui_battery_read(b) && b[0] < 15 && b[1] == 0) {
    low_power_mode();      /* not charging and low on power means it is time to economize */
}

Note: "no battery" and "0% battery" are two different things: only a return of 0 means the former. Faking it with 100 or 0 makes the program think "there is plenty of power" or "the power is gone", when the truth is "this question is meaningless here".

Note: the battery level changes very slowly (a few minutes per notch), so glancing at it in logic that already runs every tick is enough - there is no need to check it every frame.

ui_power_is_saving(void)

Is the system's power-saving mode on? -> 1/0.

Note: this is not the same thing as "the battery is low": it is the user explicitly asking to save power (and they may have turned it on manually at 40%). Lowering the frame rate because of it is doing what the user asked, rather than a guess on your part. The two are independent - the battery can be full with power saving on.

ui_battery_fetch(void) / ui_battery_get_level(void) / ui_battery_is_charging(void)

For languages that cannot get a pointer (structured like ui_touch_fetch): call ui_battery_fetch() once to query and cache, then read ui_battery_get_level() and so on.

A ready-made example: the battery line is drawn along the bottom of examples/c/app/tilt.c.

Clipboard and sharing

Standard fare for a phone game: sharing a score, and save codes (encoding a whole round into a short piece of text that the player keeps in their notes and pastes back on another device). Neither needs permissions.

char code[64];
ui_clipboard_set("SAVE-1a2b3c");                 /* hand the save code to the player */
if (ui_clipboard_get(code, 64) > 0) { /* read it back and carry on */ }
ui_share_text("I turned the screw 8 times!", "Share score");

ui_clipboard_set(char* text) / ui_clipboard_get(char* buf, int cap)

Writes / reads the system clipboard. set returns 1 = handed to the system, 0 = this platform has no clipboard; get returns the number of UTF-8 bytes written, and -1 = the clipboard is empty.

Note: a return of 1 only means "it was handed to the system", not "the player has saved it" - writing the clipboard is asynchronous on the platform, and the host does not wait for the result. Note: when the buffer is too small, get returns the number of bytes actually written (not the original length) - **compare it with strlen to tell whether it was truncated**. Note: passing an empty string to set does not clear the clipboard (it returns 0) - that is "could not work it out", not the "clear" you wanted.

ui_share_text(char* text, char* title)

Opens the system share sheet (an empty title string = no title). 1 = shown, 0 = this platform has no share sheet. Note: it does not block - it only brings the sheet up, and the program keeps running.

ui_url_open(char* url)

Opens a link in the system browser. **Only http:// and https:// are accepted**; any other scheme returns 0 (allowing any scheme would let a program use the system to open anything).

Note: it leaves this app - save your state before calling it. But do not expect the host to pause for you: that would be a policy, not a mechanism.

Note: a return of 0 means "this platform does not have that ability", not failure - a desktop has no share sheet. The game should carry on as usual (the same handling as "the device has no gyroscope").

Error codes (why the last call failed)

Camera / gallery, the file pickers and GPS all return 0 on any failure - and 0 cannot tell apart three things: the user cancelled / there is no permission / this platform simply does not have it. These two calls separate them:

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));   /* plain words, e.g. "no permission" */
    ui_dlg_msg("Could not get an image", why, VML_DLG_WARN);
}
ValueNameMeaning
0VML_E_OKno error (the last call succeeded)
1VML_E_CANCELLEDthe user cancelled - not a failure
2VML_E_PERMISSIONpermission denied (or "do not ask again")
3VML_E_UNSUPPORTEDthis platform has no such capability (no camera / GPS / sensor)
4VML_E_TIMEOUTtimed out
5VML_E_FAILEDanything else (host error / exception / missing file)

ui_error_get_code(void)

The error code of the most recent failure; 0 when there is no error.

Note: the slot describes "the call just made" - any other ui_* call clears it back to 0, so read it immediately after the failure. The two readers never clear it themselves, so asking for the code and then for the message still describes the same failure.

if (ui_gps_fetch(5000) == 0) {
    if (ui_error_get_code() == VML_E_PERMISSION) { /* point the user at the system settings */ }
}

ui_error_get_message(char* buf, int cap)

Writes a human-readable description into buf (at most cap bytes including the terminating 0) and returns the byte count; with no error it writes an empty string and returns 0. cap = 0 asks for the length only (nothing is written). The text follows the UI language, so it can be shown to the player as is.

Note: VML_E_CANCELLED is a normal player action - do not treat it as a fatal error.