ShimEngine
Auto-generated page. This reference was generated by Claude Opus 5 from
clion_game/src/script_bridge.rs
as of commit
8dc6e02.
The engine is under active development, so that source file is the authoritative
reference wherever the two disagree.
ShimEngine is the SDL3-based game engine in
clion_game. It embeds the Shim
interpreter and registers a set of native functions and globals into the script’s
root environment. Everything on this page is available to a script without any
import.
The script lifecycle
The engine loads a script file (game.shm by default) and runs it top to bottom
once. The script must define a function named loop; the engine calls it once
per frame. Loading fails with No loop function found if it is missing.
let x = 0
fn loop() {
x = x + 60 * delta
draw_rect(x, 100, 32, 32, r=255, g=64, b=64)
}
Outside of headless mode the engine watches the script file and hot-reloads it
when it changes, which recreates the interpreter and discards script state. Use
save_data/load_data to persist across reloads.
Note that these functions are specific to ShimEngine, and are not available in
the playground sandbox — the sandbox has its own, smaller drawing
API (for example it takes a color= tuple where ShimEngine takes separate r,
g, b, a channels).
Drawing
Draw calls are queued into a per-frame draw list and submitted by the engine
after loop returns. There is no explicit clear, so draw a full-screen rect
first if you want a background.
| Function | Description |
|---|---|
draw_rect(x, y, w, h, ...) | Queues a rectangle. See the kwargs below. |
draw_text(x, y, text, size=1, center=false) | Draws text with the built-in bitmap font. |
text_size(text, size=1) | Returns a (width, height) tuple for the text as it would be drawn. |
create_texture(w, h, rgba_bytes, nearest=false) | Uploads an RGBA texture and returns a texture handle. |
Rect(x1, y1, x2, y2) | Builds a UV sub-rectangle for draw_rect’s region=. |
window_size() | Returns the drawable size as a [width, height] list. |
draw_rect
x, y, w, and h are required; everything else is optional:
| Argument | Default | Meaning |
|---|---|---|
texture | none | A handle from create_texture; the rect is drawn textured. |
center | false | When true, x/y are the rect’s centre instead of its top-left corner. |
r, g, b, a | 255 each | Colour modulation per channel. |
region | whole texture | A Rect naming the UV sub-rectangle of the texture to sample. |
Colour channels accept either an integer in 0..=255 or a float in 0.0..=1.0
(floats are scaled and rounded, and both forms are clamped). That means
r=255 and r=1.0 are the same colour, but r=1 is nearly black.
fn loop() {
// Dark background
draw_rect(0, 0, 640, 480, r=17, g=24, b=39)
// A translucent red square centred at (320, 240)
draw_rect(320, 240, 64, 64, center=true, r=1.0, g=0.2, b=0.2, a=0.5)
}
draw_text and text_size
size is a scale factor on the 8px bitmap font, so size=2 draws 16px-tall
text. With center=true the text is centred on x/y rather than starting
there. text_size returns the (width, height) the same text and size would
occupy, which is useful for laying out or boxing text yourself.
fn loop() {
let label = "Score: 100"
let dim = text_size(label, size=2)
draw_rect(8, 8, dim[0] + 8, dim[1] + 8, r=0, g=0, b=0)
draw_text(12, 12, label, size=2)
}
Textures
create_texture(w, h, rgba_bytes) takes a flat list of exactly w * h * 4
numbers in RGBA order. As with colour channels, integers are treated as 0..=255
and floats as 0.0..=1.0. Passing nearest=true selects nearest-neighbour
filtering, which is what you want for pixel art. The returned handle is passed
back as draw_rect’s texture= kwarg.
// A 2x2 checkerboard, built once at load time.
let CHECKER = {
let data = []
for i in Range(0, 4) {
let v = if i == 0 or i == 3 { 255 } else { 0 }
data.append(v); data.append(v); data.append(v); data.append(255)
}
create_texture(2, 2, data, nearest=true)
}
fn loop() {
draw_rect(0, 0, 128, 128, texture=CHECKER)
}
To draw one sprite out of a sheet, pass region=Rect(x1, y1, x2, y2). The
coordinates are UVs clamped to 0.0..=1.0, with (0, 0) at the top-left and
(x2, y2) the bottom-right corner of the region.
Time
delta is a float global holding the seconds elapsed since the previous frame.
The engine updates it before each loop call, so scale per-frame motion by it
to stay frame-rate independent.
let angle = 0.0
fn loop() {
angle = angle + 1.5 * delta
}
Keyboard input
The key global names every key: key.A, key.Space, key.Left, and so on.
Each key exposes five attributes:
| Attribute | Meaning |
|---|---|
.pressed | True on every frame the key is held. |
.released | True on every frame the key is not held. |
.just_pressed | True only on the frame the key went down. |
.just_released | True only on the frame the key came up. |
.just_pressed_with_repeat | Like .just_pressed, but also fires on key-repeat after a 0.4s delay, then every 0.08s. Good for menus and text entry. |
Reading an unknown name is an error (Unknown key: ...). The available names
are:
- Letters
A–Z - Digits
Key0–Key9, and keypadKp0–Kp9 - Function keys
F1–F12 Enter,Escape,Backspace,Tab,Space,Delete,InsertMinus,Equal,LeftBracket,RightBracket,Backslash,Semicolon,Apostrophe,GraveAccent,Comma,Period,SlashLeft,Right,Up,Down,Home,End,PageUp,PageDownCapsLock,NumLock,ScrollLock,PrintScreen,PauseKpDivide,KpMultiply,KpMinus,KpPlus,KpEnter- Modifiers
LeftCtrl,LeftShift,LeftAlt,LeftSuper, and the matchingRight...variants
fn loop() {
if key.Escape.just_pressed {
// open the pause menu
}
if key.Right.pressed {
x = x + 120 * delta
}
if key.Down.just_pressed_with_repeat {
selected = selected + 1
}
}
Mouse input
| Function | Description |
|---|---|
mouse_pos() | Returns the cursor position as an [x, y] list. |
mouse_pressed(button) | True while button is held. |
mouse_just_pressed(button) | True on the frame button went down. |
mouse_just_released(button) | True on the frame button came up. |
show_cursor() / hide_cursor() | Shows or hides the system cursor. |
The button constants are MOUSE_LEFT (1), MOUSE_MIDDLE (2), MOUSE_RIGHT
(3), MOUSE_X1 (4), and MOUSE_X2 (5). Any other value is an error.
fn loop() {
let m = mouse_pos()
if mouse_just_pressed(MOUSE_LEFT) {
fire_at(m[0], m[1])
}
}
Window
| Function | Description |
|---|---|
window_size() | The drawable size as [width, height]. |
set_window_title(title) | Sets the OS window title. |
mouse_focus() | True while the window is under the mouse. |
input_focus() | True while the window has keyboard focus. |
Randomness
The engine seeds an xorshift64 generator from the system clock on first use.
| Call | Result |
|---|---|
rand() | Float in [0.0, 1.0). |
rand(hi) | Float in [0.0, hi). |
rand(lo, hi) | Float in [lo, hi). |
randi() | Non-negative integer. |
randi(hi) | Integer in [0, hi); hi must be non-negative. |
randi(lo, hi) | Integer in [lo, hi); lo must be <= hi. |
Passing more than two arguments to either is an error.
Audio
Audio is queued as commands each frame and handled by the mixer. Playback calls return a voice handle you can keep to control that one sound later.
Playing tones
play_sine(freq, duration, amp=0.5, attack=0.005, decay=0.0, sustain=1.0,
release=0.005, delay=0.0, pan=0.0, bus=0)
play_square(freq, duration, duty=0.5, amp=0.5, attack=0.005, decay=0.0,
sustain=1.0, release=0.005, delay=0.0, pan=0.0, bus=0)
freq is in Hz and duration in seconds; both are required. attack, decay,
sustain, and release are a standard ADSR envelope (sustain is a level
clamped to 0.0..=1.0, the rest are seconds). delay postpones the start by
that many seconds. pan is clamped to -1.0 (full left) through 1.0 (full
right). duty on play_square is the pulse width, clamped to 0.0..=1.0.
fn beep() {
play_sine(440.0, 0.15, amp=0.3)
}
Samples
create_sample(samples, sample_rate) registers a buffer of mono float samples
and returns a sample handle. sample_rate must be positive. The handle’s
.play() method starts a voice:
sample.play(amp=1.0, speed=1.0, fade_in=0.005, fade_out=0.005,
delay=0.0, pan=0.0, bus=0)
speed is a playback-rate multiplier (so it changes pitch too); fade_in and
fade_out are short click-avoiding ramps in seconds. .play() takes no
positional arguments and returns a voice handle.
Voice handles
| Member | Description |
|---|---|
.finished | True once the voice has stopped playing. |
.stop() | Stops the voice. |
.set_gain(amp, ramp=0.01) | Ramps the voice’s gain to amp over ramp seconds. |
.set_pan(pan, ramp=0.01) | Ramps the pan to pan (clamped to -1.0..=1.0). |
.pause(fade=0.01) | Fades out and freezes playback position. |
.resume(fade=0.01) | Fades back in from where it was paused. |
Targeting a voice that has already finished is a harmless no-op.
let music = play_sine(220.0, 30.0, amp=0.2, bus=1)
fn loop() {
if key.M.just_pressed {
music.pause()
}
if music.finished {
music = play_sine(220.0, 30.0, amp=0.2, bus=1)
}
}
Buses
Every voice is routed to one of 32 mixer buses, addressed 0 through 31
(bus=0 is the default). A bus value outside that range, or a non-integer, is
an error.
| Function | Description |
|---|---|
set_bus_gain(bus, gain, ramp=0.01) | Ramps a bus’s gain over ramp seconds. |
set_bus_lowpass(bus, cutoff, q=0.707, ramp=0.01) | Applies a low-pass filter with the given cutoff (Hz) and resonance. |
clear_bus_effects(bus) | Removes the bus’s effects. |
pause_bus(bus, fade=0.01) | Fades out and freezes every voice on the bus. |
resume_bus(bus, fade=0.01) | Resumes the bus. |
pause_audio(fade=0.01) | Pauses the whole mix. |
resume_audio(fade=0.01) | Resumes the whole mix. |
reset_audio(fade=0.05) | Stops everything and resets the audio system. |
The muffled-underwater effect is a low-pass on a bus:
let MUSIC_BUS = 1
fn duck_music() {
set_bus_lowpass(MUSIC_BUS, 500.0, ramp=0.2)
set_bus_gain(MUSIC_BUS, 0.4, ramp=0.2)
}
fn restore_music() {
clear_bus_effects(MUSIC_BUS)
set_bus_gain(MUSIC_BUS, 1.0, ramp=0.2)
}
Save data
save_data and load_data serialise Shim values to JSON on disk. Both are
desktop only — on the web build they raise
save_data is not supported on web.
save_data(data, path)
load_data(type, path, default=..., default_fn=...)
save_data writes any serialisable value: None, ints, floats, bools, strings,
tuples, lists, dicts, and structs (whose fields are saved by name). Values that
have no JSON form — functions, native handles — are an error.
load_data needs the struct type as its first argument, not just the path. The
type argument supplies the captured environment used to reconstruct nested
struct values, so the exact definition in scope is the one that gets rebuilt.
If the file is missing you can supply a fallback: default= for a ready-made
value, or default_fn= for a function called to build one. Passing both is an
error, and passing neither means a missing file is an error.
struct Game {
level = 1
score = 0
}
let state = load_data(Game, "game.sav", default_fn=Game)
fn loop() {
if key.S.just_pressed {
save_data(state, "game.sav")
}
if key.L.just_pressed {
state = load_data(Game, "game.sav", default_fn=Game)
}
}
Performance counters
The perf global reports the previous frame’s timings in seconds:
| Attribute | Meaning |
|---|---|
perf.script | Time spent running the script. |
perf.gc | Time spent in garbage collection. |
perf.render | Time spent rendering. |
perf.vsync | Time spent waiting for vsync. |
perf.total | The sum of the four above. |
perf.mem_high_point | High-water mark of interpreter memory use, in bytes (an integer). |
fn loop() {
draw_text(4, 4, "frame: \(perf.total * 1000.0)ms")
}
Debug UI
Three functions wrap Dear ImGui for quick debug panels. They mirror the
underlying immediate-mode API, so each ig_begin needs a matching ig_end.
| Function | Description |
|---|---|
ig_begin(title) | Starts a window; returns true when the window is visible. |
ig_text(text) | Draws a line of text in the current window. |
ig_end() | Ends the current window. |
Neither title nor text may contain a null byte.
fn loop() {
if ig_begin("Debug") {
ig_text("delta: \(delta)")
}
ig_end()
}