Host globals
The globals every project has on every host, without an import: timers, the frame loop, console,
the asset() macro and the angle constants. Per-frame game logic belongs in an
Aspect or a System, not a hand-written setLoop; a timer
started for a screen stops in the screen's
onClose.
- run code once after a delay, like hiding a message after 2 seconds (
setTimeout) - repeat something every second — a countdown, a clock, polling (
setInterval) - stop a timer or a frame loop (
clearTimeout,clearInterval,clearLoop) - update a value on every frame outside a game scene (
setLoop) - print a value or an object to the log while debugging (
console.log) - use an image, a sound or a JSON data file from the project folder (
asset()) - convert an angle between degrees and radians (
DEG2RAD,RAD2DEG) - reuse one set of styles on several elements (
Style<T>)
Run code after a delay: setTimeout {#timers}
To do something once after a delay — hide a message, retry a request — use setTimeout. The
delay is milliseconds, like the web (setLoop's dt is seconds); the returned id cancels it.
setTimeout(fn: () => void, ms: number): number // runs fn once, ms from now
setInterval(fn: () => void, ms: number): number // runs fn every ms until cleared
clearTimeout(id): void
clearInterval(id): voidconst status = UIText('')
const save = UIButton(UIText('Save')).onClick(() => {
status.text = 'Saved'
setTimeout(() => { status.text = '' }, 2000) // milliseconds: hide the message after 2 s
})
UIScreen(save, status).open()Timers run on the host's clock: they neither slow down nor pause with Time.scale /
Time.paused (time.md). A delay that must stop while the game is paused counts
dt in an aspect or a system.
on iOS and Android nothing runs while the app is in the background: no timer fires and
no loop ticks until the app is back. Save what matters on pause
(app.md).
Repeat every second — a countdown, a clock, polling: setInterval
To run code again and again at a fixed period — count down once per second, tick a clock, poll a
server — use setInterval(fn, ms). It fires every ms milliseconds until clearInterval(id).
const label = UIText('3').style({ fontSize: 48 })
UIScreen(label).open()
let left = 3
const timer = setInterval(() => {
left -= 1
label.text = left > 0 ? `${left}` : 'Go!'
if (left === 0) clearInterval(timer) // stop it, or it fires every second forever
}, 1000)an interval keeps firing until it is cleared, whichever screen is shown. One that
belongs to a screen starts in the screen's onOpen and is cleared in its onClose
(screen-router.md).
Timer precision: one frame
On iOS, Android, desktop, the web player and in headless tests, timers run from the frame tick,
so their resolution is one frame: setTimeout(fn, 0) runs on the next frame, and an interval
shorter than a frame fires once per frame (a 1 ms interval: 60 calls a second at 60 fps). The
delay is required there — setTimeout(fn) throws a TypeError — and the callback gets no extra
arguments. On the web (Web Lite) they are the browser's timers.
Run code every frame: setLoop
To update something on every frame outside a game scene — a running clock, a value polled from
the host — use setLoop. Game logic in a scene goes in an Aspect or a
System instead.
setLoop(fn: (dt: number) => void): number
clearLoop(id: number): voidFires every frame. dt is seconds since the previous frame (~0.016 at 60 fps) on the host's
own clock — Time.scale and Time.paused don't touch it. On the web it is
capped at 0.1 s (a tab that comes back from the background).
const clock = UIText('0.0 s')
let elapsed = 0
const loop = setLoop(dt => { // dt: seconds since the previous frame
elapsed += dt
clock.text = `${elapsed.toFixed(1)} s`
})
const stop = UIButton(UIText('Stop')).onClick(() => clearLoop(loop))
UIScreen(clock, stop).open()An aspect's or a system's update(dt) runs in the engine's frame phases (before or after the
physics step), receives the game clock (scaled, 0 while paused) and stops with its node or scene.
setLoop is for what has neither — a UI readout, polling a host value.
a loop keeps running until cleared. One started in a screen's onOpen is stopped in
its onClose, or it leaks across navigation.
Print to the log: console
To see a value while debugging, print it with console.log — warn, info and error are the
other levels.
console.log(...data) / console.warn(...) / console.info(...) / console.error(...)The typed surface — no console.table, console.time, etc. On iOS, Android, desktop and in
headless tests each call prints one line, its arguments converted to strings and joined with a
space, so an object prints as [object Object] and an array as 1,2 — log
JSON.stringify(value) to see a structure. An Error argument prints its stack on the following
lines. On the web (Web Lite) console is the browser's own.
const player = { name: 'Ann', score: 3 }
console.log('score', player.score) // score 3
console.log(player) // [object Object]
console.log(JSON.stringify(player)) // {"name":"Ann","score":3}
console.log([1, 2]) // 1,2
console.warn('low health')A flow test's expect.log matches these lines (lecodes render --logs prints them).
Load a file that ships with the project: asset()
To use an image, a sound, a model or a data file from the project folder, name it with
asset('./path') — a compile-time macro the bundler rewrites into the import of that file.
asset(path: string): string // compile-time macro — path must be a string LITERALThe string it returns is a reference to the bundled resource — a URL in a published build, a
host-local reference when a native host runs the project from disk — so treat it as opaque and
hand it to what loads it (UIImage, new AudioPlayer, Texture2D.load, Model.load, fetch, …):
const tap = new AudioPlayer(asset('./sounds/tap.wav')) // a reference to the bundled file
const screen = UIScreen(
UIImage(asset('./images/logo.png')).style({ width: 120, height: 120 }),
UIButton(UIText('Play')).onClick(() => {
tap.time = 0
tap.play()
}),
)
screen.open()It is the value import hero from './hero.png' gives too; the macro is the form that
type-checks (the project types declare no image modules). Remote https://… URLs don't need it —
pass them as plain strings.
JSON data and SVG images: what asset() returns
Two file kinds are not references, because one would be useless for them: a .json file
yields its parsed data and a .svg file an SvgSource (for UIImage and bgImage) — both
ship inside the bundle.
const levels = asset('./levels.json') // the parsed data itself: [{ "name": "Forest", … }, …]
const logo = asset('./logo.svg') // an SvgSource, for UIImage and bgImage
UIScreen(
UIImage(logo).style({ width: 64, height: 64 }),
UIText(`${levels.length} levels, the first: ${levels[0].name}`),
).open()A file in a binary format of your own is fetched — await fetch(asset('./levels/1.bin')) — and
read with res.bytes() (a Uint8Array, synchronous — a DataView over it parses the format;
network.md); res.text() stops at the first
zero byte. Data the app reads as values ships as .json.
A path without asset(): plain strings that name a file
The macro is not needed to name a file: a string literal that is the PATH of a resource file of
the project — UIImage("images/hero.png"), "./hero.png" from the file, "/sounds/tap.mp3" from
the root — is the same reference, resolved by the bundler the same way, wherever it stands (a
table of rows the screens draw and a seed stores names its pictures that way). The lookup rules
are in conventions.
A path with a / or a leading ./ that names NO file fails the compile with
file not found: images/hero.png (main.ts:1) where the SDK takes a file — UIImage, img.src,
bgImage / backgroundImage, a tab's icon, Texture.load, Texture2D.load, Model.load,
AnimationClip.load, Sound.load, new AudioPlayer / new VideoPlayer, registerFont's url,
fetchLocal, a scene's model / texture / ibl … — and stays a string anywhere else:
files.write("photos/a.jpg"), a storage key, name.endsWith("/thumb.jpg"). A bare name that
names no file stays a string. A .json path is left alone: its asset would be its data, not a
reference.
asset() build errors: a missing file, a path in a variable
- A path with no file behind it fails the build —
asset not found: ./hero.png (main.ts:3). There is no runtime fallback, so a typo or a not-yet-generated output fails the compile instead of reaching the app as a broken reference. - Because it is a macro,
asset(someVariable)fails the build too (asset() expects a string literal, found a dynamic argument). A call inside a comment or a string is text, not a macro. - Other text formats —
.txt,.csv,.md,.xml,.yaml, … — yield the file's text as a string (const rows = asset('./levels.csv').split('\n')); parse it yourself.
The bundle runs as a plain script, so every await on this page sits in an async function — see
no top-level await.
Degrees and radians: DEG2RAD, RAD2DEG
To convert an angle, multiply: degrees × DEG2RAD = radians, radians × RAD2DEG = degrees. Both
are constants the compiler substitutes (Mathf.deg2rad() / Mathf.rad2deg() do the same —
mathf.md).
DEG2RAD // π / 180 — substituted at compile time
RAD2DEG // 180 / πAngle properties are degrees (3D eulerAngles, 2D rotation); raw angle arguments are radians
(Quat.fromAxisAngle, Mat4.rotate*), and so is what Math.atan2 returns — see
conventions.
const scene = new Scene()
const arrow = Mesh.box({ material: Material.lit({ color: '#4f7cff' }), position: [0, 0, -4] })
scene.add(arrow)
scene.open()
arrow.quaternion = Quat.fromAxisAngle([0, 1, 0], 30 * DEG2RAD) // takes radians: degrees → radians
console.log(`yaw ${arrow.eulerAngles.y.toFixed(0)}`) // yaw 30 (eulerAngles are degrees)
const turn = Math.atan2(1, 1) * RAD2DEG // Math returns radians → degrees
console.log(`turn ${turn.toFixed(0)}`) // turn 45Reuse one set of styles: the Style<T> type
To share a set of styles between elements, keep it in a constant typed Style<T>: the style
object el.style(...) of that element type accepts (every prop optional, $-class blocks
included). A pure type — it exists for the editor's checking only.
const card: Style<UIColumn> = { bgColor: '#16181d', borderRadius: 12, px: 16, py: 12, gap: 4 }
UIScreen(
UIColumn(UIText('Inbox'), UIText('12 new')).style(card),
UIColumn(UIText('Sent'), UIText('3 today')).style(card),
).open()Other compile-time macros: font(), assetIcon(), EDITOR
They live with their subject: font('id') installs a font and returns its family
(fonts.md); assetIcon('pack:name') inlines an icon as an image source
(content.md); EDITOR is
true in scene-editor bundles and false, folded away, in shipped ones
(scene files).
See also
- Aspects · Systems — per-frame game logic
- UIScreen & Router — starting and stopping work with a screen
- Time — the game clock that timers are not on
- Conventions — units, paths and
asset(), no top-levelawait - Networking —
fetchand what a response holds